package de.hsma.srn.model.user;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.List;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;

import com.thoughtworks.xstream.XStream;

import de.hsma.srn.model.key.KeyBoxModel;
import de.hsma.srn.model.key.KeyPackageModel;
import de.hsma.srn.model.key.MasterKeyPackageModel;

public class UserModel {
	/*
	 * Globals
	 */
	private static final String ROOTDIR = "Users";
	
	/*
	 * Members
	 */
	private String mUserName;
	private KeyBoxModel mKeyBox;
	private String mHomePath;
	private int mPasswordHash;

	private byte[] mEncryptedPrivateKey;
	private PublicKey mPublicKey;
	
	
	/*
	 * Getter & Setter
	 */
	/**
	 * 
	 * @return
	 */
	public String getUserName() {
		return mUserName;
	}
	/**
	 * Get the home path of this user
	 * @return
	 */
	public String getHomePath() {
		return this.mHomePath;
	}
	/**
	 * Set the homepath of the current user and create the home directory if not existing
	 * @param path
	 */
	private void setHomePath(String path) {
		File homeDirectory = new File(path);
		if(!homeDirectory.exists()) {
			homeDirectory.mkdir();
		}
		this.mHomePath = path;
	}
	/**
	 * 
	 * @return
	 */
	public KeyBoxModel getKeyBox() {
		if(this.mKeyBox == null) {
			this.mKeyBox = new KeyBoxModel(this.mUserName);
		}
		return mKeyBox;
	}
	public void setKeyBox(KeyBoxModel keyBox) {
		if(keyBox != null) {
			this.mKeyBox = keyBox;
		}
	}
	/**
	 * 
	 * @return
	 */
	public PublicKey getPublicKey() {
		return mPublicKey;
	}
	/**
	 * Returns the private key of the user.
	 * Password is needed to decrypt the private key.
	 * If the password is wrong, null will be returned!
	 * @param password for decrypting the private key
	 * @return the private key or null if passoword was wrong
	 */
	private PrivateKey getPrivateKey(String password) {
		String phrase = this.mUserName + password;
		if(phrase.hashCode() == this.mPasswordHash) {
			/*
			 *  Now decrypt the private key with password based encryption
			 */
			PBEKeySpec pbeKeySpec;
	        PBEParameterSpec pbeParamSpec;
	        SecretKeyFactory keyFac;
	        byte[] serializedPrivateKey;
	        
	        // Salt
	        byte[] salt = {
	            (byte)0xc7, (byte)0x73, (byte)0x21, (byte)0x8c,
	            (byte)0x7e, (byte)0xc8, (byte)0xee, (byte)0x99
	        };
	        
	        // Iteration count
	        int count = 20;
	        
	        // Create PBE parameter set
	        pbeParamSpec = new PBEParameterSpec(salt, count);
	        
	        // Convert the password into a SecretKey object, using a PBE key factory.
	        pbeKeySpec = new PBEKeySpec(password.toCharArray());
	        SecretKey pbeKey;
	        try {
				keyFac = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
				pbeKey = keyFac.generateSecret(pbeKeySpec);
				
				// Create PBE Cipher
		        Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");

		        // Initialize PBE Cipher with key and parameters
		        pbeCipher.init(Cipher.DECRYPT_MODE, pbeKey, pbeParamSpec);
		        
		        // NOW ENCODE THE PRIVATE KEY FINALLY
		        serializedPrivateKey = pbeCipher.doFinal(this.mEncryptedPrivateKey);
			} catch (NoSuchAlgorithmException e1) {
				throw new RuntimeException("Could not find the encryption algorithm!", e1);
			} catch (Exception e) {
				throw new RuntimeException("Could not encrypt the pleasured password of the current user!", e);
			}
	        return (PrivateKey) deserialize(serializedPrivateKey);
		}
		return null;
	}
	
	
	/*
	 * Constructors
	 */
	/**
	 * Creates a new User with given user name and password
	 * @param userName to use
	 * @param password will never be saved, so keep safe
	 */
	public UserModel(String userName, String password) {
		super();
		this.mUserName = userName;
		this.setHomePath(ROOTDIR + "/" + userName);
		this.mKeyBox = new KeyBoxModel(this.mUserName);
		
		// Generate the password hash
		this.mPasswordHash = (this.mUserName + password).hashCode();
		
		// Generate the private and public keys ...
		KeyPairGenerator kpg;
		try {
			kpg = KeyPairGenerator.getInstance("RSA");
		} catch (Exception e) {
			throw new RuntimeException("Spurious encryption mode select error", e);
		}
		kpg.initialize(2048);
		KeyPair kp = kpg.genKeyPair();
		this.mPublicKey = kp.getPublic();
		PrivateKey privateKey = kp.getPrivate();
		
		/*
		 *  Now encrypt the private key with password based encryption
		 */
		PBEKeySpec pbeKeySpec;
        PBEParameterSpec pbeParamSpec;
        SecretKeyFactory keyFac;
        
        // Serialize the private key for encryption (only byte[] allowed - no objects)
 		byte[] serializedPrivateKey = serialize(privateKey);
        
        // Salt
        byte[] salt = {
            (byte)0xc7, (byte)0x73, (byte)0x21, (byte)0x8c,
            (byte)0x7e, (byte)0xc8, (byte)0xee, (byte)0x99
        };
        
        // Iteration count
        int count = 20;
        
        // Create PBE parameter set
        pbeParamSpec = new PBEParameterSpec(salt, count);
        
        // Convert the password into a SecretKey object, using a PBE key factory.
        pbeKeySpec = new PBEKeySpec(password.toCharArray());
        SecretKey pbeKey;
        try {
			keyFac = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
			pbeKey = keyFac.generateSecret(pbeKeySpec);
			
			// Create PBE Cipher
	        Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");

	        // Initialize PBE Cipher with key and parameters
	        pbeCipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec);
	        
	        // NOW ENCODE THE PRIVATE KEY FINALLY
	        this.mEncryptedPrivateKey = pbeCipher.doFinal(serializedPrivateKey);
		} catch (NoSuchAlgorithmException e1) {
			throw new RuntimeException("Could not find the encryption algorithm!", e1);
		} catch (Exception e) {
			throw new RuntimeException("Could not encrypt the pleasured password of the current user!", e);
		}
		// At last: Generate a (new) keybox
		this.mKeyBox = new KeyBoxModel(this.mUserName);
	}

	
	/*
	 * Methods
	 */
	/**
	 * 
	 * @param filename
	 * @param sharingUsers
	 * @return
	 * @throws IOException
	 */
	public boolean encrypt(String filepath, List<UserModel> sharingUsers) {
		File sourceFile = new File(filepath);
		File tempFile = new File(filepath + ".temp");
	
		if(!sourceFile.exists() || sourceFile.isDirectory()) {
			return false;
		}
		
		InputStream in;
		OutputStream out;
		Cipher cipher;
		byte[] buffer = new byte[1024];
		int position = 0;
		byte[] encryptedFileKey;
		KeyGenerator keygen;
		SecretKey fileKey;

		try {
			in = new BufferedInputStream(new FileInputStream(sourceFile.getPath()));
			out = new BufferedOutputStream(new FileOutputStream(tempFile.getPath()));
			
			// Generate AES Key
			keygen = KeyGenerator.getInstance("AES");
			SecureRandom random = new SecureRandom();
	        keygen.init(random);
	        fileKey = keygen.generateKey();
			
	        // Encrypt file ...
			cipher = Cipher.getInstance("AES/ECB/NoPadding");
			cipher.init(Cipher.ENCRYPT_MODE, fileKey);
			while ((position = in.read(buffer)) > -1) {
				buffer = cipher.doFinal(buffer);
				out.write(buffer, 0, position);
			}
			in.close();
			out.close();
		} catch (Exception e) {
			//throw new RuntimeException("Spurious serialisation error", e);
			return false;
		}
		
		// ... delete the original and replace it with the encrypted one ...
		sourceFile.delete();
		tempFile.renameTo(sourceFile);
		
		// ... then encrypt the fileKey
		try {
			cipher = Cipher.getInstance("RSA");
			cipher.init(Cipher.WRAP_MODE, this.mPublicKey);
	        encryptedFileKey = cipher.wrap(fileKey);
		} catch (Exception e) {
			//throw new RuntimeException("Konnte FileKey nicht verschluesseln!", e);
			return false;
		}
		
        // Write this key and the file path into an new master key package and add it to my own keys
        MasterKeyPackageModel masterKeyPackage = new MasterKeyPackageModel(sourceFile.getPath(), encryptedFileKey, sharingUsers);
        this.getKeyBox().add(masterKeyPackage);
        
        // Shall i share the key yet?
        if(sharingUsers!=null && !sharingUsers.isEmpty()) {
        	PublicKey usersPublicKey;
     		KeyPackageModel newKeyPackage;
     		byte[] encryptedSymKey;
     		for(UserModel user : sharingUsers) {
     			usersPublicKey = user.getPublicKey();
     			try {
     				cipher = Cipher.getInstance("RSA");
     				cipher.init(Cipher.WRAP_MODE, usersPublicKey);
     		        encryptedSymKey = cipher.wrap(fileKey);
     		        newKeyPackage = new KeyPackageModel(filepath, encryptedSymKey);
     		        user.getKeyBox().add(newKeyPackage);
     			} catch (Exception e) {
     				//throw new RuntimeException("Konnte FileKey nicht verschluesseln!", e);
     				return false;
     			}
     		}
        }
        return true;
	}
	public boolean share(MasterKeyPackageModel keyPackage, List<UserModel> users, String password) {
		System.out.println("[MODEL] SHARING TIME!");
		if(keyPackage==null || users==null) {
			return false;
		}
		for(UserModel user : users ) {
			System.out.println("[MODEL] Gebe " + user + " einen Schluessel!");
		}
		// Remove the grant from all users
		// Optimizing: Deleting all existing keys
		KeyPackageModel existingKeyPackage;
		UserModel[] existingUsers = keyPackage.getSharedUsers().toArray(new UserModel[0]);
		for(UserModel existingUser : existingUsers) {
			existingKeyPackage = existingUser.getKeyBox().get(keyPackage.getFilePath());
			existingUser.getKeyBox().remove(existingKeyPackage);
		}
		// Decrypt the file temporarily
		PrivateKey privateKey = this.getPrivateKey(password);
		File sourceFile = new File(keyPackage.getFilePath());
		File tempFile = new File(keyPackage.getFilePath() + ".temp");
		keyPackage.decrypt(tempFile.getPath(), privateKey);
		sourceFile.delete();
		tempFile.renameTo(sourceFile);
		return this.encrypt(sourceFile.getPath(), users);
	}
	
	/*
	public boolean share(MasterKeyPackageModel keyPackage, List<UserModel> users,  String password) {
		if(keyPackage==null || users==null || users.isEmpty()) {
			return false;
		}
		Key symKey = keyPackage.getSymKey(this.getPrivateKey(password));
		if(symKey==null) {
			return false;
		}
		// Check if an user will be deleted from list - in this case the symKey has to be regenerated!
		UserModel[] existingUsers = keyPackage.getSharedUsers().toArray(new UserModel[0]);
		
		boolean userFound =  false;
		// Check if each exising user occures on the new userList
		for(UserModel existingUser : existingUsers) {
			userFound = false;
			for(UserModel user : users) {
				if(existingUser.getUserName().equalsIgnoreCase(user.getUserName())) {
					userFound = true;
				}
			}
			if(!userFound) {
				break;
			}
		}
		if(!userFound) {
			System.out.println("[MODEL] User: Removing grant from one or more users -> Removing all existing KeyPackages!");
			// Optimizing: Deleting all existing keys
			KeyPackageModel existingKeyPackage;
			for(UserModel existingUser : existingUsers) {
				existingKeyPackage = existingUser.getKeyBox().get(keyPackage.getFilePath());
				existingUser.getKeyBox().remove(existingKeyPackage);
			}
			System.out.println("[MODEL] Creating a new KeyPackage and sharing it ...");
			File tempFile = new File(keyPackage.getFilePath() + ".temp");
			//keyPackage.decrypt(keyPackage.getFilePath(), key);
			return this.encrypt(keyPackage.getFilePath(), users);
		}
		// Now encrypt for each user the symKey with his/hers public key
		System.out.println("[MODEL] Creating and sharing only copies of the existing MasterKeyPackage ...");
		String filepath = keyPackage.getFilePath();
		Cipher cipher;
		PublicKey usersPublicKey;
		KeyPackageModel newKeyPackage;
		byte[] encryptedSymKey;
		for(UserModel user : users) {
			usersPublicKey = user.getPublicKey();
			try {
				cipher = Cipher.getInstance("RSA");
				cipher.init(Cipher.WRAP_MODE, usersPublicKey);
		        encryptedSymKey = cipher.wrap(symKey);
		        newKeyPackage = new KeyPackageModel(filepath, encryptedSymKey);
		        user.getKeyBox().add(newKeyPackage);
			} catch (Exception e) {
				//throw new RuntimeException("Konnte FileKey nicht verschluesseln!", e);
				return false;
			}
		}
		// Write the "new" list to the existing master key package
		keyPackage.setSharedUsers(users);
		return true;
	}
	*/
	/**
	 * 
	 * @param key
	 */
	public boolean decrypt(KeyPackageModel key, String password, String targetFilePath) {
		if(key != null) {
			System.out.println("[MODEL] User: Encrypt " + key.getFilePath() + " to " + targetFilePath + "!");
			return key.decrypt(targetFilePath, this.getPrivateKey(password));
		}
		return false;
	}
	/**
	 * Save this user and all obtained changes
	 */
	public void save() {
		// KeyBox speichert sich selber (Selbstverwaltend)
		// Setzte vor dem Speichern deshalb die KeyBox null
		this.mKeyBox = null;
		File homeDirectory = new File("Users/" + this.mUserName);
		if(!homeDirectory.exists()) {
			homeDirectory.mkdir();
		}
		XStream stream = new XStream();
		FileOutputStream userFile;
		try {
			userFile = new FileOutputStream(homeDirectory.getPath() + "/profile");
			stream.toXML(this, userFile);
			userFile.close();
		} catch (FileNotFoundException e) {
			throw new RuntimeException("User directory not found!", e);
		} catch (IOException e) {
			throw new RuntimeException("Could not write to home directory!", e);
		}
	}
	/**
	 * Check if the given password matches
	 * @param password to check
	 * @return true if password is right
	 */
	public boolean checkPassword(String password) {
		if(this.getPrivateKey(password)!=null) {
			return true;
		}
		return false;
	}
	/**
	 * Serializes the given Object to byte-code
	 * @param obj Object to serialize
	 * @return serialized object as an byte-array
	 */
	private byte[] serialize(Object obj) {
	    ByteArrayOutputStream out = new ByteArrayOutputStream();
	    ObjectOutputStream os;
		try {
			os = new ObjectOutputStream(out);
			os.writeObject(obj);
			os.close();
		} catch (IOException e) {
			throw new RuntimeException("Spurious serialisation error", e);
		}
	    return out.toByteArray();
	}
	/**
	 * Deserializes the given byte-code into an object
	 * @param data byte-array to deserialize
	 * @return deserialized byte-code as object
	 */
	private Object deserialize(byte[] data) {
	    ByteArrayInputStream in = new ByteArrayInputStream(data);
	    ObjectInputStream is;
	    Object object;
		try {
			is = new ObjectInputStream(in);
			object = is.readObject();
			is.close();
		} catch (IOException e) {
			throw new RuntimeException("Spurious serialisation error", e);
		} catch (ClassNotFoundException e) {
			throw new RuntimeException("Spurious object casting error", e);
		}
	    return object;
	}
}
